﻿using System;
using System.Data;
using System.Text;

namespace BizElements.Core.ProviderComponents
{
    /// <summary>Generic update executer. Bulk updates are not suppported. Updates that include multiple tables are executed as atomic update statements.</summary>
    class GenericUpdater : IUpdater
    {
        /// <summary>Returns <b>false</b>. Updates that use source columns from multiple tables are executed as atomic updates, one row at a time.</summary>
        public bool ImplementsBulkUpdate
        {
            get { return false; }
        }

        #region Util.

        private static bool IsBulkUpdate(UpdateStatement update)
        {
            bool hasRelations = (update.Relations != null) && (update.Relations.Count > 0);
            return hasRelations;
        }

        #endregion

        #region RenderUpdate. Does nothing if bulk update statement is passed as parameter.

        /// <summary>Render UPDATE statement if source data doesn't belong to tables other than taret table. Does nothing if bulk update statement is passed as parameter.</summary>
        public void RenderUpdate(UpdateStatement update, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            if (IsBulkUpdate(update))
                return;

            if (update.UpdateList == null || update.UpdateList.Count == 0)
                throw new InvalidOperationException(Messages.GenericUpdater_SetListIsEmpty);

            output.Append("UPDATE ");
            update.Table.RenderTableName(dbms, output);            
            AppendUpdateList(update.UpdateList, dbms, output, parameters);
            AppendWhere(update.Where, dbms, output, parameters);
        }

        private static void AppendUpdateList(UpdateList list, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            output.Append(" SET ");
            for (int setExpressionIdx = 0; setExpressionIdx < list.Count - 1; setExpressionIdx++)
            {
                list[setExpressionIdx].Render(dbms, output, parameters);
                output.Append(", ");
            }

            list[list.Count - 1].Render(dbms, output, parameters);
        }

        private static void AppendWhere(SearchCondition where, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            bool hasFilter = (where != null) && !where.IsEmpty;
            if (hasFilter)
            {
                output.Append(" WHERE ");
                where.Render(dbms, output, parameters);
            }
        }

        #endregion

        #region Execute.

        /// <summary>Executes <see cref="UpdateStatement"/>. Bulk updates are executed in multiple round trips - one update at a time.</summary>
        public int Execute(UpdateStatement update, DbmsType dbms, IConnectionProvider conn, out CommandExecutionStatistics lastExecutedCommandInfo)
        {
            if (IsBulkUpdate(update))
                return ExecuteBulkUpdate(update, dbms, conn, out lastExecutedCommandInfo);
            else
                return ExecuteSingleTableUpdate(update, dbms, conn, out lastExecutedCommandInfo);
        }

        private int ExecuteSingleTableUpdate(UpdateStatement update, DbmsType dbms, IConnectionProvider conn, out CommandExecutionStatistics lastExecutedCommandInfo)
        {
            DbParameterCollection parameters = new DbParameterCollection();
            StringBuilder cmdText = new StringBuilder();
            RenderUpdate(update, dbms, cmdText, parameters);

            string command = cmdText.ToString();
            lastExecutedCommandInfo = new CommandExecutionStatistics(command);
            int rowsAffected = DbUtil.ExecuteNonQuery(conn, command, parameters, CommandType.Text);
            lastExecutedCommandInfo.StopTime();
            return rowsAffected;
        }

        private int ExecuteBulkUpdate(UpdateStatement update, DbmsType dbms, IConnectionProvider conn, out CommandExecutionStatistics lastExecutedCommandInfo)
        {
            StringBuilder selectCmdText = new StringBuilder();
            DbParameterCollection selectParams = new DbParameterCollection();
            RenderSelect(update, dbms, selectCmdText, selectParams);
            DataTable updateData = DbUtil.ExecuteQuery(conn, selectCmdText.ToString(), selectParams, CommandType.Text, null);

            lastExecutedCommandInfo = null;
            IDbTable updateTable = update.Table.Clone(update.Table.Alias, update.Table.ColumnAliasesArePrefixed);
            foreach (DataRow row in updateData.Rows)
            {
                UpdateStatement updateOne = CreateAtomicUpdate(update, updateTable, row);
                updateOne.Execute(conn);
                lastExecutedCommandInfo = updateOne.LastExecutedCommandInfo;
            }

            return updateData.Rows.Count;
        }

        private static UpdateStatement CreateAtomicUpdate(UpdateStatement update, IDbTable updateTable, DataRow row)
        {
            UpdateStatement updateOne = new UpdateStatement(updateTable);
            // Where PK defined/exists in DataTable.
            foreach (IDbColumn pkPart in updateTable.PrimaryKey)
            {
                object pkValue = row[pkPart.Alias];
                updateOne.Where.And(pkPart, pkValue);
            }

            foreach (UpdateExpression setExpression in update.UpdateList)
            {
                if (setExpression.ExpressionType == UpdateExpressionType.Null)
                {
                    updateOne.UpdateList.Add(UpdateExpressionFactory.Null(setExpression.Column));
                }
                else if (setExpression.ExpressionType == UpdateExpressionType.Value)
                {
                    object value = ((DbParameter)setExpression.Item).Value;
                    updateOne.UpdateList.Add(UpdateExpressionFactory.Value(setExpression.Column, value));
                }
                else if (setExpression.ExpressionType == UpdateExpressionType.OtherColumn)
                {
                    IDbColumn otherCol = setExpression.SourceColumns[0];
                    object value = row[otherCol.Alias];
                    updateOne.UpdateList.Add(UpdateExpressionFactory.Value(setExpression.Column, value));
                }
            }
            return updateOne;
        }

        #endregion

        #region RenderSelect.

        /// <summary>Creates a <see cref="SelectStatement"/> that retrieves data that is to be updated. <b>null</b> if data dont't have to be fetched, ie. if bulk update is supported when using multiple tables.</summary>
        public void RenderSelect(UpdateStatement update, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            // Al used objects must be cloned so that internal state of original objects is not changed.
            // Clone target table.
            IDbTable updateTableClone = update.Table.Clone(update.Table.Alias, update.Table.ColumnAliasesArePrefixed);
            SelectStatement select = new SelectStatement(updateTableClone);
            // Clone relations.
            select.Relations = update.Relations.Clone();
            // SearchCondition is not cloned.
            select.Where = update.Where;

            // Create select list that contains clones of all PKs of target rows and values of source columns used in set expressions.
            select.SelectList.Add(updateTableClone.PrimaryKey);
            DbRelation[] relations = select.Relations.Relations;
            foreach (UpdateExpression setExpression in update.UpdateList)
            {
                // Add source columns.
                foreach (IDbColumn originalSourceColumn in setExpression.SourceColumns)
                {
                    // Skip columns already in list.
                    if (!select.SelectList.ContainsItemWithAlias(originalSourceColumn.Alias))
                    {
                        // Find cloned table that contains current source column.
                        // Cloned tables may be on either side, ie. parent od child, in one or more of the cloned relations. Find first occurence.
                        IDbTable sourceTable;
                        DbRelation relWithSourceTbl = Array.Find<DbRelation>(relations, (r) => r.Parent.HasEqualAliasAndNameAs(originalSourceColumn.Table));
                        if (relWithSourceTbl != null)
                        {
                            sourceTable = relWithSourceTbl.Parent;
                        }
                        else
                        {
                            relWithSourceTbl = Array.Find<DbRelation>(relations, (r) => r.Child.HasEqualAliasAndNameAs(originalSourceColumn.Table));
                            sourceTable = relWithSourceTbl.Child;
                        }

                        IDbColumn sourceColClone = sourceTable.Columns.GetByColumnName(originalSourceColumn.ColumnName);
                        select.SelectList.Add(sourceColClone);
                    }
                }
            }

            select.Render(dbms, output, parameters);
        }

        #endregion
    }
}